
#
# Abstract Interface.
#

"""
Abbreviation objects are used to automatically generate context-dependent markdown content
within documentation strings. Objects of this type interpolated into docstrings will be
expanded automatically before parsing the text to markdown.

$(:FIELDS)
"""
abstract type Abbreviation end

"""
$(:SIGNATURES)

Expand the [`Abbreviation`](@ref) `abbr` in the context of the `DocStr` `doc` and write
the resulting markdown-formatted text to the `IOBuffer` `buf`.
"""
format(abbr, buf, doc) = error("`format` not implemented for `$typeof(abbr)`.")

# Only extend `formatdoc` once with our abstract type. Within the package use a different
# `format` function instead to keep things decoupled from `Base` as much as possible.
Docs.formatdoc(buf::IOBuffer, doc::Docs.DocStr, part::Abbreviation) = format(part, buf, doc)


#
# Implementations.
#


#
# `TypeFields`
#

"""
The singleton type for [`FIELDS`](@ref) abbreviations.

$(:FIELDS)
"""
struct TypeFields <: Abbreviation end

"""
An [`Abbreviation`](@ref) to include the names of the fields of a type as well as any
documentation that may be attached to the fields.

# Examples

The generated markdown text should look similar to to following example where a
type has three fields (`x`, `y`, and `z`) and the last two have documentation
attached.

```markdown

  - `x`

  - `y`

    Unlike the `x` field this field has been documented.

  - `z`

    Another documented field.
```
"""
const FIELDS = TypeFields()

function format(::TypeFields, buf, doc)
    local docs = get(doc.data, :fields, Dict())
    local binding = doc.data[:binding]
    local object = Docs.resolve(binding)
    # On 0.7 fieldnames() on an abstract type throws an error. We then explicitly return
    # an empty vector to be consistent with the behaviour on v0.6.
    # Compat necessary since Base.isabstract was introduced in v0.6.
    local fields = isabstracttype(object) ? Symbol[] : fieldnames(object)
    if !isempty(fields)
        println(buf)
        for field in fields
            print(buf, "  - `", field, "`\n")
            # Print the field docs if they exist and aren't a `doc"..."` docstring.
            if haskey(docs, field) && isa(docs[field], AbstractString)
                println(buf)
                for line in split(docs[field], "\n")
                    println(buf, isempty(line) ? "" : "    ", rstrip(line))
                end
            end
            println(buf)
        end
        println(buf)
    end
    return nothing
end


#
# `ModuleExports`
#

"""
The singleton type for [`EXPORTS`](@ref) abbreviations.

$(:FIELDS)
"""
struct ModuleExports <: Abbreviation end

"""
An [`Abbreviation`](@ref) to include all the exported names of a module is a sorted list of
`Documenter.jl`-style `@ref` links.

!!! note

    The names are sorted alphabetically and ignore leading `@` characters so that macros are
    *not* sorted before other names.

# Examples

The markdown text generated by the `EXPORTS` abbreviation looks similar to the following:

```markdown

  - [`bar`](@ref)
  - [`@baz`](@ref)
  - [`foo`](@ref)

```
"""
const EXPORTS = ModuleExports()

function format(::ModuleExports, buf, doc)
    local binding = doc.data[:binding]
    local object = Docs.resolve(binding)
    local exports = names(object)
    if !isempty(exports)
        println(buf)
        # Sorting ignores the `@` in macro names and sorts them in with others.
        for sym in sort(exports, by = s -> lstrip(string(s), '@'))
            # Skip the module itself, since that's always exported.
            sym === nameof(object) && continue
            # We print linked names using Documenter.jl cross-reference syntax
            # for ease of integration with that package.
            println(buf, "  - [`", sym, "`](@ref)")
        end
        println(buf)
    end
    return nothing
end


#
# `ModuleImports`
#

"""
The singleton type for [`IMPORTS`](@ref) abbreviations.

$(:FIELDS)
"""
struct ModuleImports <: Abbreviation end

"""
An [`Abbreviation`](@ref) to include all the imported modules in a sorted list.

# Examples

The markdown text generated by the `IMPORTS` abbreviation looks similar to the following:

```markdown

  - `Foo`
  - `Bar`
  - `Baz`

```
"""
const IMPORTS = ModuleImports()

function format(::ModuleImports, buf, doc)
    local binding = doc.data[:binding]
    local object = Docs.resolve(binding)
    local imports = unique(ccall(:jl_module_usings, Any, (Any,), object))
    if !isempty(imports)
        println(buf)
        for mod in sort(imports, by = string)
            println(buf, "  - `", mod, "`")
        end
        println(buf)
    end
end


#
# `MethodList`
#

"""
The singleton type for [`METHODLIST`](@ref) abbreviations.

$(:FIELDS)
"""
struct MethodList <: Abbreviation end

"""
An [`Abbreviation`](@ref) for including a list of all the methods that match a documented
`Method`, `Function`, or `DataType` within the current module.

# Examples

The generated markdown text will look similar to the following example where a function
`f` defines two different methods (one that takes a number, and the other a string):

````markdown
```julia
f(num)
```

defined at [`<path>:<line>`](<github-url>).

```julia
f(str)
```

defined at [`<path>:<line>`](<github-url>).
````
"""
const METHODLIST = MethodList()

function format(::MethodList, buf, doc)
    local binding = doc.data[:binding]
    local typesig = doc.data[:typesig]
    local modname = doc.data[:module]
    local func = Docs.resolve(binding)
    local groups = methodgroups(func, typesig, modname; exact = false)
    if !isempty(groups)
        println(buf)
        for group in groups
            println(buf, "```julia")
            for method in group
                printmethod(buf, binding, func, method)
                println(buf)
            end
            println(buf, "```\n")
            if !isempty(group)
                local method = group[1]
                local file = string(method.file)
                local line = method.line
                local path = cleanpath(file)
                local URL = url(method)
                isempty(URL) || println(buf, "defined at [`$path:$line`]($URL).")
            end
            println(buf)
        end
        println(buf)
    end
    return nothing
end


#
# `MethodSignatures`
#

"""
The singleton type for [`SIGNATURES`](@ref) abbreviations.

$(:FIELDS)
"""
struct MethodSignatures <: Abbreviation end

"""
An [`Abbreviation`](@ref) for including a simplified representation of all the method
signatures that match the given docstring. See [`printmethod`](@ref) for details on
the simplifications that are applied.

# Examples

The generated markdown text will look similar to the following example where a function `f`
defines method taking two positional arguments, `x` and `y`, and two keywords, `a` and the `b`.

````markdown
```julia
f(x, y; a, b...)
```
````
"""
const SIGNATURES = MethodSignatures()

function format(::MethodSignatures, buf, doc)
    local binding = doc.data[:binding]
    local typesig = doc.data[:typesig]
    local modname = doc.data[:module]
    local func = Docs.resolve(binding)
    local groups = methodgroups(func, typesig, modname)
    if !isempty(groups)
        println(buf)
        println(buf, "```julia")
        for group in groups
            for method in group
                printmethod(buf, binding, func, method)
                println(buf)
            end
        end
        println(buf, "\n```\n")
    end
end


#
# `TypeSignature`
#

"""
The singleton type for [`TYPEDEF`](@ref) abbreviations.
"""
struct TypeDefinition <: Abbreviation end

"""
An [`Abbreviation`](@ref) for including a summary of the signature of a type definition.
Some of the following information may be included in the output:

  * whether the object is an `abstract` type or a `bitstype`;
  * mutability (either `type` or `struct` is printed);
  * the unqualified name of the type;
  * any type parameters;
  * the supertype of the type if it is not `Any`.

# Examples

The generated output for a type definition such as:

```julia
\"""
\$(TYPEDEF)
\"""
struct MyType{S, T <: Integer} <: AbstractArray
    # ...
end
```

will look similar to the following:

````markdown
```julia
struct MyType{S, T<:Integer} <: AbstractArray
```
````

!!! note

    No information about the fields of the type is printed. Use the [`FIELDS`](@ref)
    abbreviation to include information about the fields of a type.
"""
const TYPEDEF = TypeDefinition()

function format(::TypeDefinition, buf, doc)
    local binding = doc.data[:binding]
    local object = gettype(Docs.resolve(binding))
    if isa(object, DataType)
        println(buf, "\n```julia")
        if isbitstype(object)
            print(buf, "bitstype ", sizeof(object) * 8, " ")
        elseif isabstracttype(object)
            print(buf, "abstract ")
        else
            print(buf, object.mutable ? "type " : "struct ")
        end
        print(buf, object.name.name)
        if !isempty(object.parameters)
            print(buf, "{")
            join(buf, object.parameters, ", ")
            print(buf, "}")
        end
        local super = supertype(object)
        super == Any ? println(buf) : println(buf, " <: ", super)
        println(buf, "```\n")
    end
end

#
# `DocStringTemplate`
#

"""
The singleton type for [`DOCSTRING`](@ref) abbreviations.
"""
struct DocStringTemplate <: Abbreviation end

"""
An [`Abbreviation`](@ref) used in [`@template`](@ref) definitions to represent the location
of the docstring body that should be spliced into a template.

!!! warning

    This abbreviation must only ever be used in template strings; never normal docstrings.
"""
const DOCSTRING = DocStringTemplate()

# NOTE: no `format` needed for this 'mock' abbreviation.
